/* Live status chip: same online window as ProfileHeader (9-16 Brno time),
   green inside it, quiet gray outside - never claims presence at 3am. */
function HeroStatus({ s }) {
  const { Icon } = window.SevcikMarketingDesignSystem_3203fa;
  const [now, setNow] = React.useState(() => new Date());
  /* On a phone the chip competes with the headline, so the English copy drops
     the city and keeps what actually changes. Czech has no short form and
     falls back to the full label. */
  const [narrow, setNarrow] = React.useState(() => typeof window !== 'undefined' && window.matchMedia('(max-width: 1024px)').matches);
  React.useEffect(() => {
    const mq = window.matchMedia('(max-width: 1024px)');
    const on = () => setNarrow(mq.matches);
    mq.addEventListener('change', on);
    return () => mq.removeEventListener('change', on);
  }, []);
  React.useEffect(() => { const id = setInterval(() => setNow(new Date()), 30000); return () => clearInterval(id); }, []);
  const h12 = s.htmlLang === 'en';
  const time = new Intl.DateTimeFormat(s.locale, { hour: h12 ? 'numeric' : '2-digit', minute: '2-digit', hour12: h12, timeZone: 'Europe/Prague' }).format(now);
  const zone = (() => {
    if (!h12) return '';
    const p = {};
    for (const x of new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Prague', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' }).formatToParts(now)) p[x.type] = x.value;
    const asUTC = Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second);
    return Math.round((asUTC - now.getTime()) / 3600000) === 2 ? 'CEST' : 'CET';
  })();
  const parts = new Intl.DateTimeFormat('en-GB', { hour: '2-digit', hourCycle: 'h23', weekday: 'short', timeZone: 'Europe/Prague' }).formatToParts(now);
  const part = (t) => (parts.find((x) => x.type === t) || {}).value;
  const hour = parseInt(part('hour'), 10);
  const weekend = part('weekday') === 'Sat' || part('weekday') === 'Sun';
  const online = !weekend && hour >= 9 && hour < 16;
  return (
    <a href={`https://wa.me/${CONTACT.whatsapp.replace(/[^\d]/g, '')}`} target="_blank" rel="noopener noreferrer" aria-label="WhatsApp"
      className={online ? 'sm-status-live' : undefined}
      style={{ display: 'inline-flex', alignItems: 'center', gap: 9, minHeight: 44, padding: '10px 16px', borderRadius: 999, background: 'rgba(255,255,255,0.16)', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)', border: '1px solid rgba(255,255,255,0.35)', color: 'var(--white)', fontFamily: 'var(--font-mono)', fontWeight: 500, fontSize: 9.5, letterSpacing: '0.06em', textTransform: 'uppercase', textDecoration: 'none', cursor: 'pointer' }}>
      <span aria-hidden="true" style={{ width: 7, height: 7, borderRadius: '50%', background: online ? 'var(--green-500)' : 'rgba(255,255,255,0.55)', animation: online ? 'sm-status-pulse 2.4s ease-in-out infinite' : 'none' }} />
      {narrow && (
        <span aria-hidden="true" style={{ display: 'inline-flex', marginLeft: -2, color: online ? 'rgba(255,255,255,0.92)' : 'rgba(255,255,255,0.5)' }}>
          <Icon name="whatsapp" size={14} color="currentColor" />
        </span>
      )}
      {online ? (narrow && s.heroStatusShort ? s.heroStatusShort : s.heroStatus) : (narrow && s.heroStatusOffShort ? s.heroStatusOffShort : s.heroStatusOff)} · {time}{zone ? ' ' + zone : ''}
    </a>
  );
}

/* The glass-tile puzzle hero. 4×5 board of rounded glass tiles cut from a
   drawn artwork (brand gradient, glowing orb under the headline's last word,
   drifting wave lines), scrambled at first paint. Two solve modes:
   - mobile (≤1024px): scroll-driven - tiles slide home as --hero-s goes 0→1
     (set by Home's scroll handler), reversible;
   - desktop: pointer-driven - sweeping the cursor latches nearby tiles home
     (one-way), a click solves the rest, and after 6s idle it begins solving
     itself so the sub-line reveal always arrives. Pointer mode WRITES the
     average solve back to --hero-s so the same CSS reveals the sub-line.
   Solved board fuses seamlessly and seals with a radial light flash. */
/* The glass-tile puzzle hero. 4×5 board of rounded glass tiles cut from a
   drawn artwork (brand gradient, glowing orb under the headline's last word,
   drifting wave lines), scrambled at first paint. Two solve modes:
   - mobile (≤1024px): scroll-driven - tiles slide home as --hero-s goes 0→1
     (set by Home's scroll handler), reversible;
   - desktop: pointer-driven - sweeping the cursor latches nearby tiles home
     (one-way), a click solves the rest, and after 6s idle it begins solving
     itself so the sub-line reveal always arrives. Pointer mode WRITES the
     average solve back to --hero-s so the same CSS reveals the sub-line.
   Solved board fuses seamlessly and seals with a radial light flash. */
const PUZZLE_SHUF = [7, 14, 11, 9, 16, 1, 19, 4, 2, 6, 13, 0, 18, 3, 10, 17, 5, 12, 15, 8];
/* Any grid other than the signed-off 4x5 gets a seeded derangement, so the
   scramble is identical on every load and no tile starts already home. */
const shufFor = n => {
  if (n === 20) return PUZZLE_SHUF;
  const p = Array.from({
    length: n
  }, (_, i) => i);
  let seed = 20260903;
  const rnd = () => {
    seed = seed * 1103515245 + 12345 & 0x7fffffff;
    return seed / 0x7fffffff;
  };
  for (let i = n - 1; i > 0; i--) {
    const k = Math.floor(rnd() * (i + 1));
    const t = p[i];
    p[i] = p[k];
    p[k] = t;
  }
  for (let i = 0; i < n; i++) if (p[i] === i) {
    const k = (i + 1) % n;
    const t = p[i];
    p[i] = p[k];
    p[k] = t;
  }
  return p;
};
function HeroPuzzle() {
  const wrapRef = React.useRef(null);
  React.useEffect(() => {
    const wrap = wrapRef.current;
    if (!wrap) return;
    const canvas = wrap.querySelector('canvas');
    const ctx = canvas.getContext('2d');
    const mq = window.matchMedia('(max-width: 1024px)');
    const BW = 400,
      BH = 500;
    let COLS = 4,
      ROWS = 5,
      CW = BW / COLS,
      CH = BH / ROWS;
    const tiles = [];
    const buildTiles = (cols, rows) => {
      COLS = cols;
      ROWS = rows;
      CW = BW / cols;
      CH = BH / rows;
      const n = cols * rows,
        shuf = shufFor(n);
      tiles.length = 0;
      for (let i = 0; i < n; i++) {
        const col = i % cols,
          row = Math.floor(i / cols);
        const scol = shuf[i] % cols,
          srow = Math.floor(shuf[i] / cols);
        tiles.push({
          x: col * CW,
          y: row * CH,
          dx: (scol - col) * CW,
          dy: (srow - row) * CH,
          st: i * 7 % 8 * 0.055,
          f: 0,
          latch: false
        });
      }
    };
    buildTiles(4, 5);
    /* ~240px per tile on screen, whatever the card measures. Only ever
       re-cut while the board is still untouched - never mid-solve. */
    const TILE_PX = 240,
      MAX_TILES = 300;
    const syncGrid = () => {
      let target = TILE_PX,
        cols = 4,
        rows = 5;
      for (let i = 0; i < 12; i++) {
        cols = Math.max(4, Math.round(BW * scale / target));
        rows = Math.max(5, Math.round(BH * scale / target));
        if (cols * rows <= MAX_TILES) break;
        target *= 1.15;
      }
      if (cols === COLS && rows === ROWS) return;
      let touched = manual;
      for (const tl of tiles) if (tl.f > 0 || tl.latch) touched = true;
      if (!touched) buildTiles(cols, rows);
    };
    let W = 0,
      H = 0,
      scale = 1,
      ox = 0,
      oy = 0,
      manual = false,
      wheelP = 0;
    let dpr = Math.min(2, window.devicePixelRatio || 1);
    const DEVICE_PX_BUDGET = 14e6;
    const syncDpr = () => {
      const base = Math.min(2, window.devicePixelRatio || 1);
      const px = Math.max(1, wrap.clientWidth) * Math.max(1, wrap.clientHeight);
      dpr = px * base * base > DEVICE_PX_BUDGET ? Math.max(0.75, Math.sqrt(DEVICE_PX_BUDGET / px)) : base;
    };
    let AS = 2;
    const art = document.createElement('canvas');
    art.width = BW * AS;
    art.height = BH * AS;
    const actx = art.getContext('2d');
    /* Board units are AS-independent (drawArt scales by AS), so nothing that
       draws needs to know this changed. Capped at 5 - beyond that the sky
       costs more to repaint than the extra detail is worth. */
    const syncArt = () => {
      const want = Math.max(2, Math.min(5, Math.ceil(scale * dpr / 2)));
      if (want === AS) return;
      AS = want;
      art.width = BW * AS;
      art.height = BH * AS;
    };
    let drawNow = null;
    const resize = () => {
      const nw = wrap.clientWidth,
        nh = wrap.clientHeight;
      if (nw === W && nh === H && canvas.width > 1) return;
      W = nw;
      H = nh;
      syncDpr();
      canvas.width = Math.max(1, W * dpr);
      canvas.height = Math.max(1, H * dpr);
      scale = Math.max(W / BW, H / BH);
      ox = (W - BW * scale) / 2;
      oy = (H - BH * scale) / 2;
      syncArt();
      syncGrid();
      /* Setting canvas.width clears the bitmap - repaint in the same frame or
         the scroll-shrink shows a black canvas on every scrolled frame. */
      if (drawNow) drawNow(performance.now());
    };
    resize();
    const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(resize) : null;
    if (ro) ro.observe(wrap);
    /* Day/night cycle: one 96s turn of the clock. The same orb is the sun by
       day (warm, wide glow) and the moon by night (pale, tight halo); stars
       fade in as the sky darkens, clouds and a line of birds only exist in
       daylight. All of it is drawn, so every puzzle tile carries its own slice
       of whatever hour it happens to be. */
    /* Five stations of the clock, each a full five-stop gradient plus the
       light level and the orb's own colour at that hour. Dawn and dusk are
       real stops, not a midpoint between night and day - that's where the
       pinks and golds live. */
    const SKY = [{
      at: 0.00,
      light: 0,
      orb: [206, 226, 255],
      c: [[4, 13, 31], [8, 33, 60], [13, 58, 82], [18, 101, 111], [47, 156, 147]]
    }, {
      at: 0.07,
      light: 0.18,
      orb: [232, 190, 190],
      c: [[16, 32, 84], [42, 62, 132], [116, 88, 150], [186, 120, 138], [92, 176, 186]]
    }, {
      at: 0.20,
      light: 0.58,
      orb: [255, 206, 120],
      c: [[26, 62, 132], [64, 112, 186], [255, 186, 88], [255, 220, 136], [64, 212, 214]]
    }, {
      at: 0.36,
      light: 0.9,
      orb: [255, 220, 170],
      c: [[42, 108, 178], [92, 162, 210], [186, 216, 224], [255, 226, 178], [96, 214, 208]]
    }, {
      at: 0.45,
      light: 1,
      orb: [255, 232, 190],
      c: [[38, 118, 190], [80, 170, 212], [152, 212, 222], [220, 232, 208], [110, 214, 196]]
    }, {
      at: 0.56,
      light: 0.94,
      orb: [255, 210, 150],
      c: [[46, 112, 176], [104, 158, 196], [196, 200, 200], [255, 214, 158], [255, 190, 136]]
    }, {
      at: 0.76,
      light: 0.45,
      orb: [255, 138, 52],
      c: [[58, 96, 158], [132, 100, 172], [240, 112, 116], [255, 142, 48], [255, 212, 122]]
    }, {
      at: 0.90,
      light: 0.12,
      orb: [226, 160, 150],
      c: [[22, 30, 78], [58, 44, 106], [128, 60, 108], [172, 84, 92], [128, 132, 150]]
    }, {
      at: 1.00,
      light: 0,
      orb: [206, 226, 255],
      c: [[4, 13, 31], [8, 33, 60], [13, 58, 82], [18, 101, 111], [47, 156, 147]]
    }];
    const STARS = [];
    for (let i = 0; i < 32; i++) {
      const r1 = Math.sin(i * 12.9898) * 43758.5453,
        r2 = Math.sin(i * 78.233) * 12345.6789,
        r3 = Math.sin(i * 39.425) * 8765.4321;
      STARS.push({
        x: Math.round((r1 - Math.floor(r1)) * BW),
        y: Math.round((r2 - Math.floor(r2)) * BH * 0.62),
        m: 0.45 + (r3 - Math.floor(r3)) * 0.55
      });
    }
    /* Interpolates the two stations either side of the current phase. */
    const skyAt = ph => {
      let i = 0;
      while (i < SKY.length - 2 && ph > SKY[i + 1].at) i++;
      const a = SKY[i],
        b = SKY[i + 1];
      const raw = (ph - a.at) / (b.at - a.at || 1);
      const t = raw * raw * (3 - 2 * raw);
      const g = actx.createLinearGradient(60 * AS, 0, 360 * AS, BH * AS);
      const off = [0, 0.3, 0.56, 0.78, 1];
      for (let k = 0; k < 5; k++) {
        const ca = a.c[k],
          cb = b.c[k];
        g.addColorStop(off[k], 'rgb(' + Math.round(ca[0] + (cb[0] - ca[0]) * t) + ',' + Math.round(ca[1] + (cb[1] - ca[1]) * t) + ',' + Math.round(ca[2] + (cb[2] - ca[2]) * t) + ')');
      }
      const orb = [0, 1, 2].map(k => Math.round(a.orb[k] + (b.orb[k] - a.orb[k]) * t));
      return {
        grad: g,
        light: a.light + (b.light - a.light) * t,
        orb
      };
    };
    const drawArt = (now, q) => {
      const tm = now / 1000;
      const phase = tm % 38 / 38;
      const sky = skyAt(phase);
      const dayP = sky.light;
      actx.setTransform(AS, 0, 0, AS, 0, 0);
      actx.fillStyle = sky.grad;
      actx.fillRect(0, 0, BW, BH);
      /* Golden hour: a warm wash low in the frame, the way a low sun warms
         the water. Peaks at dawn and dusk, absent at noon and midnight. */
      const golden = Math.max(Math.max(0, 1 - Math.abs(phase - 0.20) / 0.19), Math.max(0, 1 - Math.abs(phase - 0.76) / 0.19));
      if (golden > 0.01) {
        actx.globalCompositeOperation = 'lighter';
        const haze = actx.createLinearGradient(0, BH * 0.34, 0, BH);
        haze.addColorStop(0, 'rgba(255,150,60,0)');
        haze.addColorStop(0.55, 'rgba(255,146,54,' + (0.20 * golden).toFixed(3) + ')');
        haze.addColorStop(1, 'rgba(255,206,120,' + (0.30 * golden).toFixed(3) + ')');
        actx.fillStyle = haze;
        actx.fillRect(0, BH * 0.34, BW, BH * 0.66);
        actx.globalCompositeOperation = 'source-over';
      }
      const lift = actx.createLinearGradient(0, 0, 0, BH * 0.5);
      const la = (0.1 + 0.18 * dayP).toFixed(3);
      lift.addColorStop(0, 'rgba(180,240,255,' + la + ')');
      lift.addColorStop(1, 'rgba(180,240,255,0)');
      actx.fillStyle = lift;
      actx.fillRect(0, 0, BW, BH);
      if (q > 0.01) {
        actx.fillStyle = 'rgba(8,26,52,' + (0.46 * q).toFixed(3) + ')';
        actx.fillRect(0, 0, BW, BH);
      }
      /* Stars come out AFTER the sun has gone down and are gone before dawn
         gets going - gated on the clock, so dusk stays a clean colour field. */
      const nightA = phase > 0.88 ? Math.min(1, (phase - 0.88) / 0.07) : phase < 0.10 ? Math.min(1, (0.10 - phase) / 0.05) : 0;
      if (nightA > 0.01) {
        const sa = nightA;
        for (let i = 0; i < STARS.length; i++) {
          const s = STARS[i];
          const tw = 0.7 + 0.3 * Math.sin(tm * (0.7 + s.m) + i * 2.3);
          const sz = s.m > 0.8 ? 1.5 : 1;
          actx.fillStyle = 'rgba(255,255,255,' + (sa * s.m * tw).toFixed(3) + ')';
          actx.fillRect(s.x, s.y, sz, sz);
        }
      }
      /* Clouds: soft drifting bands, daylight only. */
      const cloudA = Math.max(0, Math.min(1, Math.min((phase - 0.28) / 0.10, (0.80 - phase) / 0.10)));
      if (cloudA > 0.01) {
        const ca = cloudA;
        for (let i = 0; i < 6; i++) {
          const cw = 116 + i * 30,
            ch = 16 + i % 3 * 7;
          const cx = (tm * (4.5 + i * 2.2) + i * 137) % (BW + cw * 2) - cw;
          const cy = 44 + i * 41;
          const cg = actx.createRadialGradient(cx, cy, 0, cx, cy, cw * 0.5);
          const cr = Math.round(255),
            cgn = Math.round(255 - 70 * golden),
            cb = Math.round(255 - 130 * golden);
          cg.addColorStop(0, 'rgba(' + cr + ',' + cgn + ',' + cb + ',' + (ca * 0.44).toFixed(3) + ')');
          cg.addColorStop(1, 'rgba(' + cr + ',' + cgn + ',' + cb + ',0)');
          actx.save();
          actx.translate(cx, cy);
          actx.scale(1, ch / (cw * 0.5));
          actx.translate(-cx, -cy);
          actx.fillStyle = cg;
          actx.beginPath();
          actx.arc(cx, cy, cw * 0.5, 0, Math.PI * 2);
          actx.fill();
          actx.restore();
        }
      }
      const desk = !mq.matches;
      const obx = desk ? 332 : 272,
        oby = desk ? 118 : 130 - 40 * (q || 0);
      /* Sun by day, moon by night - one object, two identities. */
      const gc = i => sky.orb[i];
      const glowR = 40 + 24 * dayP + 30 * golden;
      const orb = actx.createRadialGradient(obx, oby, 4, obx, oby, glowR);
      orb.addColorStop(0, 'rgba(255,' + Math.round(248 - 40 * golden) + ',' + Math.round(232 - 90 * golden) + ',' + (0.8 + 0.15 * dayP).toFixed(2) + ')');
      orb.addColorStop(0.3, 'rgba(' + gc(0) + ',' + gc(1) + ',' + gc(2) + ',' + (0.3 + 0.24 * dayP).toFixed(2) + ')');
      orb.addColorStop(1, 'rgba(' + gc(0) + ',' + gc(1) + ',' + gc(2) + ',0)');
      actx.fillStyle = orb;
      actx.beginPath();
      actx.arc(obx, oby, glowR, 0, Math.PI * 2);
      actx.fill();
      const hot = Math.max(0, dayP - 0.45) / 0.55;
      const bodyC = (i, w) => Math.round(gc(i) + (w - gc(i)) * hot);
      actx.fillStyle = 'rgba(' + bodyC(0, 255) + ',' + bodyC(1, 250) + ',' + bodyC(2, 236) + ',0.96)';
      const R = 17 + 4 * dayP + 3 * golden;
      actx.beginPath();
      actx.arc(obx, oby, R, 0, Math.PI * 2);
      actx.fill();
      /* Once the orb is the moon, give it a surface: a few soft maria and a
         little limb shading. Enough to be read as a moon, not a sticker. */
      const moonA = Math.max(0, Math.min(1, (0.22 - dayP) / 0.22));
      if (moonA > 0.01) {
        actx.save();
        actx.beginPath();
        actx.arc(obx, oby, R, 0, Math.PI * 2);
        actx.clip();
        for (const [mx, my, mr, md] of [[-0.34, -0.3, 0.36, 0.34], [0.2, -0.44, 0.2, 0.26], [0.28, 0.24, 0.42, 0.3], [-0.44, 0.36, 0.24, 0.24], [-0.04, 0.04, 0.16, 0.2]]) {
          const cx0 = obx + mx * R,
            cy0 = oby + my * R,
            r0 = mr * R;
          const mg = actx.createRadialGradient(cx0, cy0, 0, cx0, cy0, r0);
          mg.addColorStop(0, 'rgba(146,160,192,' + (md * moonA).toFixed(3) + ')');
          mg.addColorStop(0.7, 'rgba(146,160,192,' + (md * moonA * 0.5).toFixed(3) + ')');
          mg.addColorStop(1, 'rgba(146,160,192,0)');
          actx.fillStyle = mg;
          actx.beginPath();
          actx.arc(cx0, cy0, r0, 0, Math.PI * 2);
          actx.fill();
        }
        const lb = actx.createRadialGradient(obx - R * 0.32, oby - R * 0.32, R * 0.15, obx, oby, R * 1.2);
        lb.addColorStop(0, 'rgba(28,38,68,0)');
        lb.addColorStop(1, 'rgba(28,38,68,' + (0.3 * moonA).toFixed(3) + ')');
        actx.fillStyle = lb;
        actx.fillRect(obx - R, oby - R, R * 2, R * 2);
        actx.restore();
      }
      /* Birds: two loose flocks of open Vs, crossing only in daylight. */
      const birdA = Math.max(0, Math.min(1, Math.min((phase - 0.38) / 0.07, (0.68 - phase) / 0.07)));
      if (birdA > 0.01) {
        const ba = birdA;
        actx.strokeStyle = 'rgba(255,255,255,' + (ba * 0.5).toFixed(3) + ')';
        actx.lineWidth = 1.1;
        actx.lineCap = 'round';
        for (let i = 0; i < 8; i++) {
          const lane = i < 3 ? 0 : i < 6 ? 1 : 2;
          const bx = (tm * (10 + lane * 4.5) + i * 41) % (BW + 150) - 75;
          const by = [92, 170, 248][lane] + Math.sin(tm * 0.7 + i) * 7 + i % 4 * 11;
          const w = 5 + i % 3;
          const flap = 2.4 + Math.sin(tm * 3.4 + i * 1.9) * 1.9;
          actx.beginPath();
          actx.moveTo(bx - w, by - flap);
          actx.lineTo(bx, by);
          actx.lineTo(bx + w, by - flap);
          actx.stroke();
        }
      }
      /* A jet crosses once every 19s, taking ~9s about it, with a vapour
         trail that fades out behind. Daylight only. */
      const PLANE_T = 19,
        pp = tm % PLANE_T / PLANE_T;
      if (dayP > 0.35 && pp < 0.5) {
        const k = pp / 0.5;
        const px = -40 + k * (BW + 80),
          py = 58 + k * 26;
        const fade = Math.min(1, Math.min(k, 1 - k) / 0.14) * Math.min(1, (dayP - 0.35) / 0.25);
        if (fade > 0.01) {
          const len = 104,
            gap = 4.6;
          const bx0 = px - gap,
            by0 = py - gap * 0.054;
          const tx = bx0 - len,
            ty = by0 - len * 0.054;
          const tg = actx.createLinearGradient(tx, ty, bx0, by0);
          tg.addColorStop(0, 'rgba(255,255,255,0)');
          tg.addColorStop(1, 'rgba(255,255,255,' + (0.3 * fade).toFixed(3) + ')');
          actx.strokeStyle = tg;
          actx.lineWidth = 1.2;
          actx.lineCap = 'round';
          actx.beginPath();
          actx.moveTo(tx, ty);
          actx.lineTo(bx0, by0);
          actx.stroke();
          actx.save();
          actx.translate(px, py);
          actx.rotate(0.054);
          actx.strokeStyle = 'rgba(255,255,255,' + (0.82 * fade).toFixed(3) + ')';
          actx.lineWidth = 0.75;
          actx.lineCap = 'round';
          actx.lineJoin = 'round';
          actx.beginPath();
          actx.moveTo(-3.4, 0);
          actx.lineTo(2.8, 0);
          actx.moveTo(-2.3, -2.7);
          actx.lineTo(-0.2, 0);
          actx.lineTo(-2.3, 2.7);
          actx.moveTo(-3.9, -1.3);
          actx.lineTo(-3.0, 0);
          actx.lineTo(-3.9, 1.3);
          actx.stroke();
          actx.restore();
        }
      }
    };
    const rr = (x, y, w, h, r) => {
      ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(x, y, w, h, r);else ctx.rect(x, y, w, h);
    };
    let raf,
      flashAt = 0,
      armed = true;
    const draw = now => {
      const q = parseFloat(document.documentElement.style.getPropertyValue('--hero-p')) || 0;
      drawArt(now, q);
      /* Curtain: the board is laid out against the card's FULL height and
         anchored to its top, so as the intro pushes the card up the bottom
         is simply rolled out of view - nothing rescales, nothing deforms,
         the orb stays perfectly round. */
      const H0 = H + 0.44 * window.innerHeight * q;
      const s0 = Math.max(W / BW, H0 / BH);
      const ox0 = (W - BW * s0) / 2,
        oy0 = (H0 - BH * s0) / 2;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.fillStyle = '#08182e';
      ctx.fillRect(0, 0, W, H);
      ctx.setTransform(dpr * s0, 0, 0, dpr * s0, ox0 * dpr, oy0 * dpr);
      let anyGap = false;
      for (const tl of tiles) if (tl.f < 0.999) {
        anyGap = true;
        break;
      }
      if (anyGap) {
        ctx.globalAlpha = 0.26;
        ctx.drawImage(art, 0, 0, BW * AS, BH * AS, 0, 0, BW, BH);
        ctx.globalAlpha = 1;
      }
      let minF = 1,
        sumF = 0;
      for (const tl of tiles) {
        if (tl.f < minF) minF = tl.f;
        sumF += tl.f;
      }
      /* Solved board = the art, uncut. One blit instead of a clip and a blit
         per tile, which is the whole cost of sitting at rest. */
      if (minF >= 0.999) {
        ctx.setTransform(dpr * s0, 0, 0, dpr * s0, ox0 * dpr, oy0 * dpr);
        ctx.drawImage(art, 0, 0, BW * AS, BH * AS, 0, 0, BW, BH);
      } else for (const tl of tiles) {
        const f = tl.f;
        const g = 1 - f,
          u = CW / 100;
        const inset = (3 * g - 0.4 * f) * u,
          rad = 12 * u * Math.pow(g, 1.5);
        ctx.setTransform(dpr * s0, 0, 0, dpr * s0, (ox0 + tl.dx * g * s0) * dpr, (oy0 + tl.dy * g * s0) * dpr);
        ctx.save();
        rr(tl.x + inset, tl.y + inset, CW - inset * 2, CH - inset * 2, rad);
        ctx.clip();
        const sx = Math.max(0, (tl.x + inset) * AS),
          sy = Math.max(0, (tl.y + inset) * AS);
        const sw = Math.min(BW * AS - sx, (CW - inset * 2) * AS),
          sh = Math.min(BH * AS - sy, (CH - inset * 2) * AS);
        ctx.drawImage(art, sx, sy, sw, sh, sx / AS, sy / AS, sw / AS, sh / AS);
        if (g > 0.01) {
          const sheen = ctx.createLinearGradient(tl.x, tl.y, tl.x, tl.y + CH);
          sheen.addColorStop(0, 'rgba(255,255,255,' + (0.12 * g).toFixed(3) + ')');
          sheen.addColorStop(0.55, 'rgba(255,255,255,0)');
          ctx.fillStyle = sheen;
          ctx.fillRect(tl.x, tl.y, CW, CH);
        }
        ctx.restore();
        if (g > 0.01) {
          rr(tl.x + inset, tl.y + inset, CW - inset * 2, CH - inset * 2, rad);
          /* Two plain strokes fake the seam glow - shadowBlur here cost more
             than the whole rest of the frame and read as scroll lag. */
          ctx.strokeStyle = 'rgba(255,255,255,' + (0.14 * g).toFixed(3) + ')';
          ctx.lineWidth = 2.6 / s0;
          ctx.stroke();
          ctx.strokeStyle = 'rgba(255,255,255,' + (0.34 * g).toFixed(3) + ')';
          ctx.lineWidth = 0.7 / s0;
          ctx.stroke();
        }
      }
      /* Legibility veil (desktop pointer mode ONLY - the mobile fold is
         signed off without it): a dark wash over the scrambled board that
         lifts as the puzzle solves, so the headline wins at rest. */
      const veil = mq.matches ? 0 : 0.34 * (1 - sumF / tiles.length);
      if (veil > 0.005) {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.fillStyle = 'rgba(10,8,20,' + veil.toFixed(3) + ')';
        ctx.fillRect(0, 0, W, H);
      }
      if (minF >= 1) {
        if (armed) {
          flashAt = now;
          armed = false;
        }
      } else if (minF < 0.6) {
        armed = true;
      }
      if (!armed && flashAt) {
        const ft = (now - flashAt) / 750;
        if (ft < 1) {
          const a = 0.5 * (1 - ft) * (1 - ft);
          ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
          ctx.globalCompositeOperation = 'lighter';
          const fg = ctx.createRadialGradient(W / 2, H * 0.42, 0, W / 2, H * 0.42, Math.max(W, H) * 0.75);
          fg.addColorStop(0, 'rgba(255,255,255,' + a.toFixed(3) + ')');
          fg.addColorStop(1, 'rgba(255,255,255,0)');
          ctx.fillStyle = fg;
          ctx.fillRect(0, 0, W, H);
          ctx.globalCompositeOperation = 'source-over';
        }
      }
    };
    drawNow = draw;
    /* Pointer mode (desktop): sweep latches tiles near the cursor, click
       latches everything, 6s idle starts a gentle self-solve. Latched tiles
       ease home one-way; the average is published to --hero-s for the CSS
       sub-line reveal. */
    const latchNear = (px, py) => {
      if (mq.matches) return;
      const bx = (px - ox) / scale,
        by = (py - oy) / scale;
      for (const tl of tiles) {
        if (tl.latch) continue;
        const g = 1 - tl.f;
        const cx = tl.x + CW / 2 + tl.dx * g,
          cy = tl.y + CH / 2 + tl.dy * g;
        if (Math.hypot(bx - cx, by - cy) < Math.max(CW, CH) * 1.18) tl.latch = true;
      }
    };
    /* Wheel scrub (desktop): the board becomes a dial - wheel down builds it,
       wheel up takes it apart again. Taking over cancels the idle self-solve
       and the sweep latches, so the reader is in charge from then on. */
    wheelP = 0;
    const onWheel = e => {
      if (mq.matches) return;
      e.preventDefault();
      if (!manual) {
        manual = true;
        clearTimeout(idleTo);
        clearInterval(autoInt);
        let sum = 0;
        for (const tl of tiles) sum += tl.f;
        wheelP = sum / tiles.length;
      }
      wheelP = Math.max(0, Math.min(1, wheelP + e.deltaY * 0.0034));
    };
    const onMove = e => {
      if (manual) return;
      const r = wrap.getBoundingClientRect();
      latchNear(e.clientX - r.left, e.clientY - r.top);
    };
    const onClick = () => {
      if (mq.matches) return;
      if (manual) {
        wheelP = wheelP > 0.5 ? 0 : 1;
        return;
      }
      tiles.forEach(tl => {
        tl.latch = true;
      });
    };
    /* Listen on the CARD, not the canvas: the scrim and the headline block
       are painted above the board, so events must be caught as they bubble. */
    const host = wrap.parentElement || wrap;
    host.addEventListener('mousemove', onMove);
    host.addEventListener('click', onClick);
    host.addEventListener('wheel', onWheel, {
      passive: false
    });
    let autoInt = 0;
    const idleTo = setTimeout(() => {
      autoInt = setInterval(() => {
        if (mq.matches) return;
        const open = tiles.filter(tl => !tl.latch);
        if (!open.length) {
          clearInterval(autoInt);
          return;
        }
        /* Two at a time, so the desktop board resolves in a couple of seconds
           instead of drifting for ten. */
        open[Math.floor(Math.random() * open.length)].latch = true;
        if (open.length > 1) open[Math.floor(Math.random() * open.length)].latch = true;
      }, 190);
    }, 1600);
    let lastPv = null,
      lastDrawn = 0,
      prevT = 0;
    const loop = now => {
      /* Clamped, so coming back to a backgrounded tab eases in rather than
         snapping. */
      const dt = prevT ? Math.min(0.05, (now - prevT) / 1000) : 1 / 60;
      prevT = now;
      let moving = false;
      const scrollMode = mq.matches;
      const sVal = parseFloat(document.documentElement.style.getPropertyValue('--hero-s')) || 0;
      /* Tiles EASE toward their target instead of snapping to it - the
         glide outlives a fast flick by ~half a second, which is what makes
         the solve read as deliberate rather than instant. Reversible in
         scroll mode; one-way latches in pointer mode. */
      for (const tl of tiles) {
        const p = scrollMode ? sVal : wheelP;
        const target = scrollMode || manual ? Math.max(0, Math.min(1, (p - tl.st) / 0.45)) : tl.latch ? 1 : 0;
        const d = target - tl.f;
        if (Math.abs(d) > 0.0008) {
          const rate = scrollMode ? 5.7 : manual ? 14.9 : 7;
          tl.f += d * (1 - Math.exp(-rate * dt));
          moving = true;
        } else {
          tl.f = target;
        }
      }
      if (!scrollMode) {
        let sum = 0;
        for (const tl of tiles) sum += tl.f;
        const avg = (Math.round(sum / tiles.length * 200) / 200).toFixed(3);
        if (document.documentElement.style.getPropertyValue('--hero-s') !== avg) document.documentElement.style.setProperty('--hero-s', avg);
        /* Once the copy has been revealed, it stays - taking the board apart
           again shouldn't hide what the reader has already read. */
        if (parseFloat(avg) > 0.88) document.documentElement.classList.add('sm-hero-seen');
      }
      const st = document.documentElement.style;
      const pv = (st.getPropertyValue('--hero-s') || '0') + '|' + (st.getPropertyValue('--hero-p') || '0');
      /* Composite every frame - the sky itself only repaints at 30fps, so a
         full-rate loop costs one blit and removes the stutter that the old
         20fps idle throttle was adding.
         A throw inside draw() used to break the rAF chain and freeze the
         whole hero - never again. */
      lastPv = pv;
      lastDrawn = now;
      try {
        draw(now);
      } catch (err) {
        console.error('hero draw', err);
      }
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => {
      cancelAnimationFrame(raf);
      if (ro) ro.disconnect();
      host.removeEventListener('mousemove', onMove);
      host.removeEventListener('click', onClick);
      host.removeEventListener('wheel', onWheel);
      clearTimeout(idleTo);
      if (autoInt) clearInterval(autoInt);
    };
  }, []);
  return (
    <div ref={wrapRef} className="sm-hero-puzzle" style={{ position: 'absolute', inset: 0, display: 'none', background: '#171429', overflow: 'hidden' }}>
      <canvas style={{ display: 'block', width: '100%', height: '100%' }}></canvas>
    </div>
  );
}

function Home({ s, t }) {
  const [a, em1, b, d, em2, e] = s.hero.lead;
  /* Two-phase scroll (mobile): first ~5svh of scroll solves the puzzle at
     full height (--hero-s 0→1, card never resizes - no canvas clears, no
     lag), the next ~44svh shrinks the card (--hero-p 0→1) at EXACTLY scroll
     rate (44svh shrink over 44svh of scroll), so the gap to the intro stays
     a constant 18px - any rate mismatch reads as a spring. THREE constants
     must stay in step: this span, the -44svh height term in index.html, and
     the 0.44 H0 compensation in draw(). The solve span
     doubles as the intro peek's approach distance, so keep them in step
     with the CSS in index.html. */
  React.useEffect(() => {
    const on = () => {
      if (window.matchMedia('(min-width: 1025px)').matches) return;
      const y = window.scrollY || 0;
      const solveSpan = window.innerHeight * 0.05, shrinkSpan = window.innerHeight * 0.44;
      const st = document.documentElement.style;
      const sNow = Math.max(0, Math.min(1, y / solveSpan));
      st.setProperty('--hero-s', sNow.toFixed(4));
      if (sNow > 0.88) document.documentElement.classList.add('sm-hero-seen');
      st.setProperty('--hero-p', Math.max(0, Math.min(1, (y - solveSpan) / shrinkSpan)).toFixed(4));
    };
    on();
    window.addEventListener('scroll', on, { passive: true });
    window.addEventListener('resize', on);
    return () => { window.removeEventListener('scroll', on); window.removeEventListener('resize', on); };
  }, []);
  const glass = { display: 'flex', alignItems: 'center', gap: 9, minHeight: 44, padding: '11px 18px', borderRadius: 999, background: 'rgba(255,255,255,0.16)', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)', border: '1px solid rgba(255,255,255,0.35)', color: 'var(--white)', fontFamily: 'var(--font-meta)', fontWeight: 500, fontSize: 14.5, textDecoration: 'none' };
  return (
    <div className="sm-home-main" style={{ flex: '1 1 0%', minWidth: 0, display: 'flex', position: 'relative' }}>
      {/* Ambient glow: the hero's own gradient bleeding past the card edge,
         the way product pages let the screen light the page around it. */}
      <div aria-hidden="true" style={{ position: 'absolute', inset: '26px 26px 26px 16px', background: 'linear-gradient(150deg, #0a1f3c 0%, #12658c 55%, #63d3c0 100%)', filter: 'blur(48px)', opacity: 0.4, borderRadius: 48, pointerEvents: 'none' }}></div>
      <div className="sm-content" style={{ flex: 1, margin: '10px 10px 10px 0', borderRadius: 'var(--radius-card)', overflow: 'hidden', position: 'relative', minHeight: 0 }}>
        <video
          className="sm-hero-video"
          autoPlay muted loop playsInline preload="metadata"
          style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
        >
          <source src="../../assets/video/hero-water-loop.webm" type="video/webm" />
        </video>
        <div className="sm-hero-tint" style={{ position: 'absolute', inset: 0, background: 'linear-gradient(150deg, #0a1f3c 0%, #12658c 55%, #63d3c0 100%)', opacity: 0.62, mixBlendMode: 'multiply' }} />
        <PathHero />
        <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0) 40%)' }} />
        <div className="sm-hero" style={{ position: 'relative', height: '100%', padding: '40px 72px 64px', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 40 }}>
          <h1 style={{ margin: 0, flexShrink: 0, fontFamily: 'var(--font-display)', fontWeight: 600, fontSize: 'clamp(' + (s.hero.headMin || '52px') + ', 6.2vw, 104px)', lineHeight: 0.96, letterSpacing: '-0.03em', color: 'var(--white)', maxWidth: s.hero.headWidth || '9ch', overflowWrap: 'break-word', textIndent: '-0.055em' }}>
            {a}<em style={{ fontStyle: 'italic', fontWeight: 500 }}>{em1}</em>{b}<br />{d}<span className="sm-hero-together"><em style={{ fontStyle: 'italic', fontWeight: 500 }}>{em2}</em>{e}</span>
          </h1>
          <div className="sm-hero-spacer" style={{ flex: 1 }} />
          <div style={{ flexShrink: 0, maxWidth: 'min(660px, 92%)' }}>
            {s.hero.sub && <p className="sm-hero-sub" style={{ margin: 0, fontFamily: 'var(--font-display)', fontWeight: 500, fontSize: 22, lineHeight: 'var(--leading-snug)', color: 'var(--white)' }}>{s.hero.sub}</p>}
            <p className={'sm-hero-body' + (s.hero.sub ? '' : ' sm-hero-solo')} style={{ margin: 0, marginTop: s.hero.sub ? 20 : 0, fontFamily: 'var(--font-body)', fontSize: 15, color: 'rgba(255,255,255,0.7)', lineHeight: 1.6 }}>{s.hero.bodyMobile && <span className="sm-hero-body-short">{s.hero.bodyMobile}</span>}<span className={s.hero.bodyMobile ? 'sm-hero-body-long' : undefined}>{String(s.hero.body).split('\n').map((line, i) => (i === 0 ? <React.Fragment key={i}>{line}</React.Fragment> : <span key={i} className="sm-hero-line-extra"><br />{line}</span>))}</span></p>
          </div>
          <div className="sm-hero-ctas" style={{ display: 'none', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
            <HeroStatus s={s} />
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { Home });
